📁 File Handling & Data Processing

Python Avanzato - Gestione Completa di File e Dati

🎯Introduzione alla Lezione

Benvenuto in questa lezione approfondita sul File Handling e il Data Processing in Python! In questo modulo esploreremo uno degli aspetti più cruciali della programmazione moderna: la capacità di leggere, scrivere, elaborare e gestire dati provenienti da diverse fonti e in diversi formati.

Ogni applicazione reale, dalle app mobili ai sistemi enterprise, ha bisogno di persistere i dati, scambiarli con altri sistemi e processarli in modo efficiente. Imparerai a gestire file di testo semplici, formati strutturati come JSON e XML, database tabulari come CSV, configurazioni in YAML, e molto altro ancora.

Obiettivi della lezione:

  • ✅ Padroneggiare la lettura e scrittura di file TXT
  • ✅ Lavorare con formati di dati strutturati (JSON, CSV, XML, YAML)
  • ✅ Utilizzare Pandas per l'analisi dati (introduzione)
  • ✅ Implementare un robusto sistema di gestione errori
  • ✅ Configurare il logging professionale per le tue applicazioni

📄1. File di Testo (TXT)

🎭 Analogia: Il File di Testo come un Quaderno

Pensa a un file di testo come a un quaderno tradizionale. Puoi aprirlo, leggere le pagine una per una, aggiungere nuove pagine alla fine, oppure strappare tutto e ricominciare da capo. Il file TXT è il formato più semplice e universale: nessuna formattazione, solo caratteri puri.

Come in un quaderno devi sapere in che lingua è scritto (l'encoding), così nei file TXT devi specificare la codifica dei caratteri (UTF-8, ASCII, etc.).

1.1 Modalità di Apertura File

Python offre diverse modalità per aprire un file, ciascuna con uno scopo specifico. È fondamentale scegliere la modalità corretta per evitare di perdere dati o causare errori.

Modalità Nome Descrizione File deve esistere?
r Read Solo lettura. Il cursore è all'inizio. ✅ Sì
w Write Scrittura. CANCELLA il contenuto esistente! ❌ No (lo crea)
a Append Aggiunge alla fine senza cancellare. ❌ No (lo crea)
r+ Read+Write Lettura e scrittura. ✅ Sì
x Exclusive Create Crea file nuovo, errore se esiste già. ❌ No (fallisce se esiste)
⚠️ Attenzione alla modalità 'w'!
La modalità w è distruttiva: se il file esiste già, tutto il suo contenuto viene immediatamente cancellato! Usa sempre a (append) se vuoi preservare i dati esistenti.

1.2 Lettura di File TXT

Ci sono diversi metodi per leggere un file di testo in Python. La scelta dipende dalle tue esigenze: vuoi leggere tutto in una volta, riga per riga, o in blocchi?

# Metodo 1: Leggere tutto il file in una stringa
with open('dati.txt', 'r', encoding='utf-8') as file:
    contenuto = file.read()
    print(contenuto)

# Metodo 2: Leggere tutte le righe in una lista
with open('dati.txt', 'r', encoding='utf-8') as file:
    righe = file.readlines()  # Ogni riga include \n
    for riga in righe:
        print(riga.strip())  # strip() rimuove \n e spazi

# Metodo 3: Leggere riga per riga (EFFICIENTE per file grandi)
with open('log_gigante.txt', 'r', encoding='utf-8') as file:
    for riga in file:  # Iterazione lazy, non carica tutto in memoria!
        if 'ERROR' in riga:
            print(riga.strip())

# Metodo 4: Leggere carattere per carattere (raro)
with open('dati.txt', 'r', encoding='utf-8') as file:
    while True:
        char = file.read(1)
        if not char:
            break
        print(char, end='')

🔑 Concetto Chiave: Il Context Manager (with)

Il costrutto with è fondamentale quando lavori con i file. È un context manager che garantisce che il file venga chiuso automaticamente anche se si verificano errori durante l'esecuzione del codice.

Senza with:

file = open('dati.txt', 'r')
contenuto = file.read()
file.close()  # Devi ricordarti di chiuderlo!

Con with (RACCOMANDATO):

with open('dati.txt', 'r') as file:
    contenuto = file.read()
# Il file si chiude automaticamente qui!

1.3 Scrittura di File TXT

# Scrittura completa (SOVRASCRIVE il file)
with open('output.txt', 'w', encoding='utf-8') as file:
    file.write("Prima riga\n")
    file.write("Seconda riga\n")
    
    # Scrivere più righe insieme
    righe = ["Riga 3\n", "Riga 4\n", "Riga 5\n"]
    file.writelines(righe)

# Append (aggiunge senza cancellare)
with open('log.txt', 'a', encoding='utf-8') as file:
    import datetime
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    file.write(f"{timestamp} - Evento importante\n")

# Esempio pratico: salvare una lista di nomi
nomi = ["Alice", "Bob", "Charlie"]
with open('nomi.txt', 'w', encoding='utf-8') as file:
    for nome in nomi:
        file.write(f"{nome}\n")
💡 Best Practice: Encoding UTF-8
Specifica sempre encoding='utf-8' quando lavori con file di testo! UTF-8 è lo standard universale che supporta tutti i caratteri internazionali (lettere accentate, emoji, caratteri cinesi, etc.). Senza specificarlo, Python potrebbe usare l'encoding predefinito del sistema operativo, causando problemi di compatibilità.

1.4 Esempio Completo: Sistema di Log Personalizzato

import datetime

class SimpleLogger:
    """Logger semplice che scrive su file"""
    
    def __init__(self, filename):
        self.filename = filename
    
    def log(self, message, level="INFO"):
        """Scrive un messaggio di log con timestamp"""
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_entry = f"{timestamp} [{level}] {message}\n"
        
        with open(self.filename, 'a', encoding='utf-8') as file:
            file.write(log_entry)
    
    def read_logs(self, filter_level=None):
        """Legge e filtra i log"""
        try:
            with open(self.filename, 'r', encoding='utf-8') as file:
                for line in file:
                    if filter_level is None or filter_level in line:
                        print(line.strip())
        except FileNotFoundError:
            print("Nessun file di log trovato.")

# Utilizzo
logger = SimpleLogger("app.log")
logger.log("Applicazione avviata")
logger.log("Database connesso", "INFO")
logger.log("Errore di connessione", "ERROR")

# Leggi solo gli errori
print("\n=== ERRORI ===")
logger.read_logs(filter_level="ERROR")

📝 Quiz #1 - File TXT

Quale modalità di apertura devi usare per aggiungere dati alla fine di un file di log senza cancellare il contenuto esistente?
  • A) w (write)
  • B) r+ (read+write)
  • C) a (append)
  • D) x (exclusive create)

🔗2. JavaScript Object Notation (JSON)

🎭 Analogia: JSON come una Scatola Etichettata

Immagina JSON come un sistema di scatole ben organizzate dove ogni scatola ha un'etichetta (chiave) e un contenuto (valore). Le scatole possono contenere oggetti semplici (numeri, stringhe) o altre scatole annidate, creando una gerarchia ordinata.

JSON è il formato universale per lo scambio di dati sul web. API REST, configurazioni, database NoSQL... ovunque guardi, trovi JSON!

2.1 Struttura JSON

JSON supporta sei tipi di dati fondamentali:

Tipo JSON Tipo Python Esempio
Object dict {"nome": "Alice"}
Array list [1, 2, 3]
String str "Hello"
Number int/float 42, 3.14
Boolean bool true, false
Null None null
⚠️ Differenze Sintattiche Python vs JSON:
  • JSON usa true/false, Python usa True/False
  • JSON usa null, Python usa None
  • JSON richiede virgolette doppie per le stringhe: "nome" non 'nome'

2.2 Lavorare con JSON in Python

import json

# ==== SERIALIZZAZIONE: Python → JSON ====

# Dizionario Python
persona = {
    "nome": "Mario Rossi",
    "età": 30,
    "città": "Roma",
    "hobby": ["calcio", "lettura", "programmazione"],
    "sposato": True,
    "figli": None
}

# Convertire in stringa JSON
json_string = json.dumps(persona, indent=4, ensure_ascii=False)
print(json_string)

# Salvare su file
with open('persona.json', 'w', encoding='utf-8') as file:
    json.dump(persona, file, indent=4, ensure_ascii=False)

# ==== DESERIALIZZAZIONE: JSON → Python ====

# Leggere da stringa JSON
json_str = '{"nome": "Alice", "età": 25}'
dati = json.loads(json_str)
print(dati["nome"])  # Alice

# Leggere da file
with open('persona.json', 'r', encoding='utf-8') as file:
    persona_caricata = json.load(file)
    print(persona_caricata["hobby"])

🔑 Differenza tra dump/dumps e load/loads

  • dump: Scrive Python → JSON in un file
  • dumps: Converte Python → JSON in una stringa (dump string)
  • load: Legge JSON → Python da un file
  • loads: Converte JSON → Python da una stringa (load string)

Memorizza: la "s" finale significa "string"!

2.3 Parametri Utili di json.dumps()

import json

dati = {"nome": "Élise", "città": "Parigi", "età": 28}

# indent: formattazione leggibile (per debug)
json_bello = json.dumps(dati, indent=2)

# ensure_ascii: False per caratteri non-ASCII (accenti, emoji)
json_utf8 = json.dumps(dati, ensure_ascii=False)

# sort_keys: ordina le chiavi alfabeticamente
json_ordinato = json.dumps(dati, sort_keys=True)

# Combinazione comune per produzione
json.dumps(dati, indent=4, ensure_ascii=False, sort_keys=True)

2.4 Esempio Completo: API Configuration Manager

import json
import os

class ConfigManager:
    """Gestisce configurazioni in formato JSON"""
    
    def __init__(self, config_file="config.json"):
        self.config_file = config_file
        self.config = self.load_config()
    
    def load_config(self):
        """Carica la configurazione dal file"""
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        else:
            # Configurazione di default
            return {
                "database": {
                    "host": "localhost",
                    "port": 5432,
                    "name": "mydb"
                },
                "api": {
                    "url": "https://api.example.com",
                    "timeout": 30
                },
                "logging": {
                    "level": "INFO",
                    "file": "app.log"
                }
            }
    
    def save_config(self):
        """Salva la configurazione su file"""
        with open(self.config_file, 'w', encoding='utf-8') as f:
            json.dump(self.config, f, indent=4, ensure_ascii=False)
    
    def get(self, key_path):
        """Ottiene un valore usando notazione punto: 'database.host'"""
        keys = key_path.split('.')
        value = self.config
        for key in keys:
            value = value.get(key)
            if value is None:
                return None
        return value
    
    def set(self, key_path, new_value):
        """Imposta un valore usando notazione punto"""
        keys = key_path.split('.')
        config = self.config
        for key in keys[:-1]:
            config = config.setdefault(key, {})
        config[keys[-1]] = new_value
        self.save_config()

# Utilizzo
config = ConfigManager()

# Leggere configurazioni
db_host = config.get("database.host")
print(f"Database host: {db_host}")

# Modificare configurazioni
config.set("api.timeout", 60)
config.set("database.password", "secret123")

📝 Quiz #2 - JSON

Quale delle seguenti affermazioni su JSON è FALSA?
  • A) JSON usa virgolette doppie per le stringhe
  • B) JSON supporta i commenti
  • C) json.loads() converte una stringa JSON in Python
  • D) ensure_ascii=False preserva caratteri non-ASCII

📊3. Comma-Separated Values (CSV)

🎭 Analogia: CSV come un Foglio Excel Semplificato

Pensa al CSV come a un foglio di calcolo salvato come testo puro. Ogni riga è un record, e le celle sono separate da virgole (o altri delimitatori come il punto e virgola). È perfetto per dati tabulari: elenchi di utenti, transazioni, dati scientifici, esportazioni da database.

CSV è il formato più comune per l'import/export di dati tra diverse applicazioni (Excel, Google Sheets, database, applicazioni Python).

3.1 Struttura CSV

Un file CSV tipico ha questa struttura:

# File: studenti.csv
nome,cognome,età,voto
Mario,Rossi,20,28
Alice,Bianchi,22,30
Luigi,Verdi,21,27
Sara,Neri,23,29
⚠️ Attenzione ai Delimitatori Regionali!
In Italia e in molti paesi europei, Excel usa il punto e virgola (;) come delimitatore invece della virgola, perché la virgola è usata come separatore decimale (3,14 invece di 3.14). Specifica sempre il delimitatore corretto quando lavori con CSV!

3.2 Lettura CSV

import csv

# ==== METODO 1: Leggere come lista di liste ====
with open('studenti.csv', 'r', encoding='utf-8') as file:
    csv_reader = csv.reader(file)
    
    # Salta l'header
    header = next(csv_reader)
    print(f"Colonne: {header}")
    
    # Leggi i dati
    for row in csv_reader:
        nome, cognome, età, voto = row
        print(f"{nome} {cognome}: voto {voto}")

# ==== METODO 2: Leggere come dizionari (RACCOMANDATO) ====
with open('studenti.csv', 'r', encoding='utf-8') as file:
    csv_reader = csv.DictReader(file)
    
    for row in csv_reader:
        # Accesso per chiave invece che per indice!
        print(f"{row['nome']} {row['cognome']}: voto {row['voto']}")

# ==== Con delimitatore personalizzato (Excel italiano) ====
with open('dati_excel.csv', 'r', encoding='utf-8') as file:
    csv_reader = csv.DictReader(file, delimiter=';')
    for row in csv_reader:
        print(row)

3.3 Scrittura CSV

import csv

# ==== METODO 1: Scrivere liste ====
studenti = [
    ["Mario", "Rossi", 20, 28],
    ["Alice", "Bianchi", 22, 30],
    ["Luigi", "Verdi", 21, 27]
]

with open('output.csv', 'w', newline='', encoding='utf-8') as file:
    csv_writer = csv.writer(file)
    
    # Scrivi header
    csv_writer.writerow(["nome", "cognome", "età", "voto"])
    
    # Scrivi dati
    csv_writer.writerows(studenti)

# ==== METODO 2: Scrivere dizionari (RACCOMANDATO) ====
studenti_dict = [
    {"nome": "Mario", "cognome": "Rossi", "età": 20, "voto": 28},
    {"nome": "Alice", "cognome": "Bianchi", "età": 22, "voto": 30}
]

with open('output_dict.csv', 'w', newline='', encoding='utf-8') as file:
    fieldnames = ["nome", "cognome", "età", "voto"]
    csv_writer = csv.DictWriter(file, fieldnames=fieldnames)
    
    csv_writer.writeheader()  # Scrive automaticamente l'header
    csv_writer.writerows(studenti_dict)

🔑 Perché newline=''?

Il parametro newline='' è necessario su Windows per evitare righe vuote extra nel CSV. Il modulo csv gestisce già le newline internamente, quindi dobbiamo dire a open() di non aggiungerne di proprie. Su Linux/Mac non fa differenza, ma è buona pratica includerlo sempre.

3.4 Esempio Completo: Analisi Vendite

import csv
from collections import defaultdict

class SalesAnalyzer:
    """Analizza dati di vendita da file CSV"""
    
    def __init__(self, csv_file):
        self.csv_file = csv_file
        self.sales = []
        self.load_sales()
    
    def load_sales(self):
        """Carica i dati di vendita dal CSV"""
        with open(self.csv_file, 'r', encoding='utf-8') as file:
            csv_reader = csv.DictReader(file)
            for row in csv_reader:
                self.sales.append({
                    'prodotto': row['prodotto'],
                    'quantità': int(row['quantità']),
                    'prezzo': float(row['prezzo']),
                    'data': row['data']
                })
    
    def total_revenue(self):
        """Calcola il fatturato totale"""
        return sum(sale['quantità'] * sale['prezzo'] for sale in self.sales)
    
    def sales_by_product(self):
        """Raggruppa vendite per prodotto"""
        product_sales = defaultdict(lambda: {'quantità': 0, 'ricavo': 0})
        
        for sale in self.sales:
            prod = sale['prodotto']
            product_sales[prod]['quantità'] += sale['quantità']
            product_sales[prod]['ricavo'] += sale['quantità'] * sale['prezzo']
        
        return dict(product_sales)
    
    def export_report(self, output_file):
        """Esporta un report aggregato in CSV"""
        sales_data = self.sales_by_product()
        
        with open(output_file, 'w', newline='', encoding='utf-8') as file:
            fieldnames = ['prodotto', 'quantità_totale', 'ricavo_totale']
            writer = csv.DictWriter(file, fieldnames=fieldnames)
            
            writer.writeheader()
            for product, data in sales_data.items():
                writer.writerow({
                    'prodotto': product,
                    'quantità_totale': data['quantità'],
                    'ricavo_totale': round(data['ricavo'], 2)
                })

# Utilizzo
analyzer = SalesAnalyzer('vendite.csv')
print(f"Fatturato totale: €{analyzer.total_revenue():.2f}")
analyzer.export_report('report_vendite.csv')

📝 Quiz #3 - CSV

Quale vantaggio offre csv.DictReader rispetto a csv.reader?
  • A) È più veloce per file grandi
  • B) Permette l'accesso ai campi per nome invece che per indice
  • C) Supporta più delimitatori simultaneamente
  • D) Converte automaticamente i tipi di dati

🌳4. eXtensible Markup Language (XML)

🎭 Analogia: XML come un Albero Genealogico

XML è come un albero genealogico dove ogni persona (elemento) può avere:

  • Nome (tag): es. <persona>
  • Caratteristiche (attributi): es. età="30"
  • Figli (elementi annidati): es. <figlio>
  • Informazioni (testo): il contenuto tra i tag

XML è verboso ma estremamente flessibile e auto-descrittivo. È usato in configurazioni complesse, feed RSS, documenti Office, e molte API legacy.

4.1 Struttura XML

<!-- File: biblioteca.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<biblioteca nome="Biblioteca Comunale">
    <libro isbn="978-0134685991">
        <titolo>Effective Python</titolo>
        <autore>
            <nome>Brett</nome>
            <cognome>Slatkin</cognome>
        </autore>
        <anno>2019</anno>
        <disponibile>true</disponibile>
    </libro>
    <libro isbn="978-1491946008">
        <titolo>Fluent Python</titolo>
        <autore>
            <nome>Luciano</nome>
            <cognome>Ramalho</cognome>
        </autore>
        <anno>2015</anno>
        <disponibile>false</disponibile>
    </libro>
</biblioteca>

4.2 Parsing XML con ElementTree

import xml.etree.ElementTree as ET

# ==== PARSING DA FILE ====
tree = ET.parse('biblioteca.xml')
root = tree.getroot()

print(f"Root tag: {root.tag}")  # biblioteca
print(f"Root attrib: {root.attrib}")  # {'nome': 'Biblioteca Comunale'}

# ==== ITERARE SU ELEMENTI ====
for libro in root.findall('libro'):
    isbn = libro.get('isbn')
    titolo = libro.find('titolo').text
    anno = libro.find('anno').text
    disponibile = libro.find('disponibile').text == 'true'
    
    # Accesso annidato
    autore = libro.find('autore')
    nome_autore = autore.find('nome').text
    cognome_autore = autore.find('cognome').text
    
    print(f"{titolo} by {nome_autore} {cognome_autore} ({anno}) - {'Disponibile' if disponibile else 'Non disponibile'}")

# ==== PARSING DA STRINGA ====
xml_string = """
<catalogo>
    <prodotto id="1">
        <nome>Laptop</nome>
        <prezzo>999.99</prezzo>
    </prodotto>
</catalogo>
"""
root = ET.fromstring(xml_string)

# ==== TROVARE ELEMENTI CON XPATH ====
# Trova tutti i titoli
titoli = root.findall('.//titolo')  # . = ricerca ricorsiva
for titolo in titoli:
    print(titolo.text)

4.3 Creare e Modificare XML

import xml.etree.ElementTree as ET
from xml.dom import minidom

# ==== CREARE XML DA ZERO ====
root = ET.Element("libreria")
root.set("nome", "Libreria Moderna")

libro1 = ET.SubElement(root, "libro")
libro1.set("isbn", "123-456")

titolo1 = ET.SubElement(libro1, "titolo")
titolo1.text = "Python Essentials"

prezzo1 = ET.SubElement(libro1, "prezzo")
prezzo1.text = "29.99"

# ==== FORMATTARE (prettify) XML ====
def prettify(elem):
    """Formatta XML in modo leggibile"""
    rough_string = ET.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

print(prettify(root))

# ==== SALVARE SU FILE ====
tree = ET.ElementTree(root)
tree.write("libreria_output.xml", encoding="utf-8", xml_declaration=True)

# ==== MODIFICARE XML ESISTENTE ====
tree = ET.parse('biblioteca.xml')
root = tree.getroot()

# Cambia il nome della biblioteca
root.set('nome', 'Biblioteca Nazionale')

# Aggiungi un nuovo libro
nuovo_libro = ET.SubElement(root, 'libro')
nuovo_libro.set('isbn', '978-1234567890')

titolo = ET.SubElement(nuovo_libro, 'titolo')
titolo.text = 'Learning Python'

tree.write('biblioteca_aggiornata.xml', encoding='utf-8', xml_declaration=True)
💡 XPath in ElementTree
ElementTree supporta un subset limitato di XPath:
  • findall('libro') - trova tutti i figli diretti chiamati 'libro'
  • findall('.//libro') - trova tutti i 'libro' ricorsivamente
  • findall('libro[@isbn]') - trova libri con attributo isbn
  • find('libro/titolo') - trova il primo titolo dentro un libro

📝 Quiz #4 - XML

In ElementTree, quale metodo useresti per trovare TUTTI gli elementi con un certo tag in modo ricorsivo?
  • A) find('tag')
  • B) findall('.//tag')
  • C) get('tag')
  • D) search('tag')

⚙️5. YAML Ain't Markup Language (YAML)

🎭 Analogia: YAML come un Documento Leggibile

YAML è come scrivere una configurazione in linguaggio naturale. Niente parentesi graffe, niente virgole obbligatorie, niente tag verbosi. Solo indentazione pulita e sintassi minimalista.

Se JSON è il linguaggio delle macchine, YAML è il linguaggio degli umani. È il formato preferito per file di configurazione (Docker, Kubernetes, CI/CD pipelines) perché è facilissimo da leggere e scrivere a mano.

5.1 Sintassi YAML

YAML usa l'indentazione (SPAZI, non tab!) per rappresentare la gerarchia:

# File: config.yaml

# Commenti iniziano con #

# Chiave-valore semplici
nome: "Mario Rossi"
età: 30
attivo: true

# Liste (array)
hobby:
  - calcio
  - lettura
  - programmazione

# Oppure inline
colori: [rosso, verde, blu]

# Dizionari annidati
indirizzo:
  via: "Via Roma 123"
  città: "Milano"
  cap: "20100"

# Multi-line strings
descrizione: |
  Questa è una stringa
  su più righe.
  Le newline sono preservate.

# Riferimenti (anchors)
defaults: &defaults
  timeout: 30
  retry: 3

api_config:
  <<: *defaults  # Eredita da defaults
  url: "https://api.example.com"
⚠️ Attenzione agli Spazi!
YAML è estremamente sensibile all'indentazione. Usa sempre spazi (di solito 2 o 4), mai tabulazioni! Un'indentazione sbagliata cambia completamente la struttura del documento.

5.2 Lavorare con YAML in Python

Python non include YAML nella libreria standard. Dobbiamo installare PyYAML:

# Installazione
pip install pyyaml
import yaml

# ==== LETTURA YAML ====
with open('config.yaml', 'r', encoding='utf-8') as file:
    config = yaml.safe_load(file)  # SEMPRE safe_load per sicurezza!
    
    print(config['nome'])
    print(config['hobby'])
    print(config['indirizzo']['città'])

# ==== SCRITTURA YAML ====
dati = {
    'database': {
        'host': 'localhost',
        'port': 5432,
        'credentials': {
            'username': 'admin',
            'password': 'secret123'
        }
    },
    'features': ['cache', 'logging', 'monitoring'],
    'debug_mode': True
}

with open('output.yaml', 'w', encoding='utf-8') as file:
    yaml.dump(dati, file, default_flow_style=False, allow_unicode=True)

# ==== PARSING DA STRINGA ====
yaml_string = """
server:
  host: 127.0.0.1
  port: 8000
"""
config = yaml.safe_load(yaml_string)
print(config['server']['host'])

🔑 safe_load vs load

SEMPRE usare safe_load()! La funzione load() può eseguire codice Python arbitrario incorporato nel YAML (per motivi di retrocompatibilità), rappresentando un grave rischio di sicurezza.

safe_load() carica solo strutture dati standard (dict, list, str, int, float, bool, None) ed è completamente sicuro.

5.3 Esempio Completo: Gestione Configurazione Applicazione

import yaml
import os

class AppConfig:
    """Gestisce la configurazione dell'applicazione da file YAML"""
    
    def __init__(self, config_file="config.yaml"):
        self.config_file = config_file
        self.config = self.load()
    
    def load(self):
        """Carica la configurazione"""
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        else:
            # Configurazione di default
            return self.get_default_config()
    
    def get_default_config(self):
        """Ritorna la configurazione predefinita"""
        return {
            'app': {
                'name': 'MyApp',
                'version': '1.0.0',
                'debug': False
            },
            'database': {
                'host': 'localhost',
                'port': 5432,
                'name': 'mydb',
                'pool_size': 10
            },
            'logging': {
                'level': 'INFO',
                'file': 'app.log',
                'max_size_mb': 10,
                'backup_count': 5
            },
            'features': {
                'cache_enabled': True,
                'cache_ttl': 3600,
                'api_rate_limit': 100
            }
        }
    
    def get(self, key_path, default=None):
        """Ottiene un valore usando notazione punto: 'database.host'"""
        keys = key_path.split('.')
        value = self.config
        try:
            for key in keys:
                value = value[key]
            return value
        except (KeyError, TypeError):
            return default
    
    def set(self, key_path, new_value):
        """Imposta un valore usando notazione punto"""
        keys = key_path.split('.')
        config = self.config
        for key in keys[:-1]:
            config = config.setdefault(key, {})
        config[keys[-1]] = new_value
    
    def save(self):
        """Salva la configurazione su file"""
        with open(self.config_file, 'w', encoding='utf-8') as f:
            yaml.dump(self.config, f, default_flow_style=False, allow_unicode=True)
    
    def display(self):
        """Mostra la configurazione in formato leggibile"""
        print(yaml.dump(self.config, default_flow_style=False, allow_unicode=True))

# Utilizzo
config = AppConfig()

# Leggi configurazioni
db_host = config.get('database.host')
log_level = config.get('logging.level')
print(f"Database: {db_host}, Log Level: {log_level}")

# Modifica configurazione
config.set('app.debug', True)
config.set('features.new_feature', True)

# Salva
config.save()

# Mostra tutto
config.display()

📝 Quiz #5 - YAML

Perché è SEMPRE raccomandato usare yaml.safe_load() invece di yaml.load()?
  • A) safe_load() è più veloce
  • B) safe_load() previene l'esecuzione di codice arbitrario
  • C) safe_load() supporta più tipi di dati
  • D) safe_load() gestisce meglio gli errori di sintassi

🛡️6. Exception Handling Avanzato

🎭 Analogia: Le Eccezioni come Semafori

Le eccezioni sono come semafori e segnali stradali nel tuo codice. Quando qualcosa va storto (file non trovato, connessione persa, dato invalido), Python solleva un'eccezione per fermare il flusso normale e avvisarti del problema.

Un buon programmatore non ignora i semafori rossi: intercetta le eccezioni, le gestisce appropriatamente, e garantisce che l'applicazione continui a funzionare (o fallisca in modo controllato).

6.1 Gerarchia delle Eccezioni

Python ha una gerarchia di eccezioni ben strutturata. Ecco le più comuni:

Eccezione Quando viene sollevata Esempio
FileNotFoundError File non trovato open('inesistente.txt')
ValueError Valore inappropriato int('abc')
KeyError Chiave non in dizionario d['chiave_inesistente']
IndexError Indice fuori range lista[100]
TypeError Operazione su tipo sbagliato 'testo' + 5
ZeroDivisionError Divisione per zero 10 / 0
AttributeError Attributo inesistente obj.metodo_inesistente()
IOError / OSError Errore I/O generico Problemi disco, permessi

6.2 Try-Except-Else-Finally

try:
    # Codice che potrebbe sollevare un'eccezione
    file = open('dati.txt', 'r')
    contenuto = file.read()
    numero = int(contenuto)
    risultato = 100 / numero
    
except FileNotFoundError:
    # Gestisci file non trovato
    print("Errore: File non trovato!")
    
except ValueError:
    # Gestisci conversione fallita
    print("Errore: Il file non contiene un numero valido!")
    
except ZeroDivisionError:
    # Gestisci divisione per zero
    print("Errore: Il numero è zero!")
    
except Exception as e:
    # Catch-all per eccezioni non previste
    print(f"Errore inaspettato: {e}")
    
else:
    # Eseguito SOLO se non ci sono eccezioni
    print(f"Successo! Risultato: {risultato}")
    
finally:
    # Eseguito SEMPRE, anche con eccezioni o return
    try:
        file.close()
        print("File chiuso.")
    except:
        pass

🔑 Quando Usare Else e Finally

  • else: Codice che deve essere eseguito solo se non ci sono eccezioni. È utile per separare la logica di successo dalla gestione errori.
  • finally: Codice di cleanup che deve essere eseguito sempre (chiudere file, rilasciare risorse, log di audit). Viene eseguito anche con return o break!

6.3 Eccezioni Multiple e Cattura Specifica

# Catturare più eccezioni con lo stesso handler
try:
    valore = dizionario[chiave]
    risultato = int(valore) / divisore
except (KeyError, ValueError, ZeroDivisionError) as e:
    print(f"Errore: {type(e).__name__} - {e}")

# Ottenere informazioni dettagliate sull'eccezione
import traceback

try:
    # Codice rischioso
    risultato = funzione_complessa()
except Exception as e:
    print(f"Tipo: {type(e).__name__}")
    print(f"Messaggio: {str(e)}")
    print(f"Args: {e.args}")
    print("\nStack trace completo:")
    traceback.print_exc()
⚠️ Evita Bare Except!
Non usare mai except: senza specificare l'eccezione! Questo cattura TUTTO, incluso KeyboardInterrupt (Ctrl+C) e SystemExit, rendendo impossibile interrompere il programma. Usa sempre except Exception: come minimo.

6.4 Creare Eccezioni Custom

class DatabaseError(Exception):
    """Eccezione base per errori database"""
    pass

class ConnectionError(DatabaseError):
    """Errore di connessione al database"""
    pass

class QueryError(DatabaseError):
    """Errore nell'esecuzione di una query"""
    
    def __init__(self, query, message):
        self.query = query
        self.message = message
        super().__init__(f"Query failed: {message}\nQuery: {query}")

class ValidationError(Exception):
    """Errore di validazione dati"""
    
    def __init__(self, field, value, reason):
        self.field = field
        self.value = value
        self.reason = reason
        super().__init__(f"Validation error in '{field}': {reason} (value: {value})")

# Utilizzo
def validate_age(age):
    if not isinstance(age, int):
        raise ValidationError("age", age, "must be an integer")
    if age < 0 or age > 150:
        raise ValidationError("age", age, "must be between 0 and 150")
    return True

try:
    validate_age("trent")
except ValidationError as e:
    print(f"Campo: {e.field}")
    print(f"Valore: {e.value}")
    print(f"Motivo: {e.reason}")

6.5 Context Manager Personalizzati

class FileManager:
    """Context manager per gestione file con logging"""
    
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        """Chiamato all'ingresso del blocco with"""
        print(f"Apertura file: {self.filename}")
        try:
            self.file = open(self.filename, self.mode, encoding='utf-8')
            return self.file
        except Exception as e:
            print(f"Errore apertura: {e}")
            raise
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Chiamato all'uscita del blocco with (anche con eccezioni)"""
        if self.file:
            self.file.close()
            print(f"File chiuso: {self.filename}")
        
        if exc_type is not None:
            print(f"Si è verificata un'eccezione: {exc_type.__name__}: {exc_val}")
        
        # Return False per propagare l'eccezione, True per sopprimerla
        return False

# Utilizzo
with FileManager('test.txt', 'w') as f:
    f.write("Hello, World!")
    # File chiuso automaticamente anche se ci sono errori

📝 Quiz #6 - Exception Handling

Quando viene eseguito il blocco else in una struttura try-except-else-finally?
  • A) Sempre, dopo finally
  • B) Solo se si verifica un'eccezione
  • C) Solo se NON si verifica alcuna eccezione
  • D) Prima del blocco try

📝7. Logging Professionale

🎭 Analogia: Il Logging come un Diario di Bordo

Il logging è come il diario di bordo di una nave: registra tutti gli eventi importanti, errori, decisioni e milestone. Quando qualcosa va storto in produzione, il log è la tua scatola nera per capire cosa è successo.

A differenza di print(), il logging è strutturato, ha livelli di severità, può salvare su file, ruotare automaticamente i log, e molto altro. È uno strumento indispensabile per applicazioni professionali.

7.1 Livelli di Logging

Python definisce 5 livelli standard di logging, in ordine crescente di severità:

Livello Valore Numerico Quando Usarlo Esempio
DEBUG 10 Informazioni dettagliate per diagnostica Parametri funzione, valori variabili
INFO 20 Conferma funzionamento normale "Server avviato", "Connessione OK"
WARNING 30 Qualcosa inaspettato, ma gestibile "Disco quasi pieno", "Richiesta lenta"
ERROR 40 Errore che impedisce una funzione "Query fallita", "File corrotto"
CRITICAL 50 Errore grave che blocca l'applicazione "Database irraggiungibile", "Out of memory"

7.2 Logging Base

import logging

# ==== CONFIGURAZIONE BASE ====
logging.basicConfig(
    level=logging.DEBUG,  # Livello minimo da mostrare
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# ==== UTILIZZO ====
logging.debug("Questo è un messaggio di debug")
logging.info("Applicazione avviata")
logging.warning("Attenzione: memoria al 90%")
logging.error("Errore durante la connessione")
logging.critical("Sistema in crash!")

# ==== LOGGING CON VARIABILI ====
username = "Mario"
action = "login"
logging.info(f"User {username} performed action: {action}")

# Metodo OLD-STYLE (ancora supportato)
logging.info("User %s performed action: %s", username, action)

7.3 Logging su File

import logging

# ==== CONFIGURAZIONE CON FILE ====
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log', encoding='utf-8'),  # Su file
        logging.StreamHandler()  # Anche su console
    ]
)

# ==== FILE ROTATION (rotazione automatica) ====
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    'app.log',
    maxBytes=10*1024*1024,  # 10 MB
    backupCount=5,  # Mantiene 5 backup
    encoding='utf-8'
)

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[handler]
)

# ==== TIME-BASED ROTATION (un file al giorno) ====
from logging.handlers import TimedRotatingFileHandler

handler = TimedRotatingFileHandler(
    'app.log',
    when='midnight',  # Ruota a mezzanotte
    interval=1,  # Ogni giorno
    backupCount=30,  # Mantiene 30 giorni
    encoding='utf-8'
)

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[handler]
)

7.4 Logger Personalizzati e Moduli

# ==== FILE: database.py ====
import logging

# Crea un logger specifico per questo modulo
logger = logging.getLogger(__name__)  # __name__ = 'database'

def connect():
    logger.info("Connessione al database...")
    # ...
    logger.info("Connessione stabilita")

def query(sql):
    logger.debug(f"Esecuzione query: {sql}")
    try:
        # Esegui query
        pass
    except Exception as e:
        logger.error(f"Query fallita: {e}", exc_info=True)  # exc_info aggiunge stack trace

# ==== FILE: main.py ====
import logging
import database

# Configurazione globale
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)  # __name__ = '__main__'

logger.info("Applicazione avviata")
database.connect()
database.query("SELECT * FROM users")

# Output:
# 2024-01-15 10:30:00 - __main__ - INFO - Applicazione avviata
# 2024-01-15 10:30:01 - database - INFO - Connessione al database...
# 2024-01-15 10:30:01 - database - INFO - Connessione stabilita
# 2024-01-15 10:30:01 - database - DEBUG - Esecuzione query: SELECT * FROM users

🔑 Perché Usare logging.getLogger(__name__)?

Ogni modulo dovrebbe avere il proprio logger ottenuto con getLogger(__name__). Questo crea una gerarchia di logger che riflette la struttura del tuo progetto:

  • myapp (root logger del progetto)
  • myapp.database (logger del modulo database)
  • myapp.api (logger del modulo api)
  • myapp.api.auth (logger del sottomodulo auth)

Puoi poi configurare livelli diversi per ogni parte dell'applicazione!

7.5 Configurazione Avanzata con Dizionario

import logging
import logging.config

# Configurazione completa come dizionario
LOGGING_CONFIG = {
    'version': 1,
    'disable_existing_loggers': False,
    
    'formatters': {
        'standard': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        },
        'detailed': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
        },
    },
    
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'INFO',
            'formatter': 'standard',
            'stream': 'ext://sys.stdout'
        },
        'file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'level': 'DEBUG',
            'formatter': 'detailed',
            'filename': 'app.log',
            'maxBytes': 10485760,  # 10 MB
            'backupCount': 5,
            'encoding': 'utf-8'
        },
        'error_file': {
            'class': 'logging.FileHandler',
            'level': 'ERROR',
            'formatter': 'detailed',
            'filename': 'errors.log',
            'encoding': 'utf-8'
        }
    },
    
    'loggers': {
        'myapp': {
            'level': 'DEBUG',
            'handlers': ['console', 'file', 'error_file'],
            'propagate': False
        },
        'myapp.database': {
            'level': 'INFO',  # Database: solo INFO+
            'propagate': True  # Propaga al parent 'myapp'
        }
    },
    
    'root': {
        'level': 'INFO',
        'handlers': ['console']
    }
}

# Applica la configurazione
logging.config.dictConfig(LOGGING_CONFIG)

# Usa i logger configurati
logger = logging.getLogger('myapp')
logger.info("App started")

db_logger = logging.getLogger('myapp.database')
db_logger.debug("Questo NON verrà loggato")  # level è INFO
db_logger.info("Query eseguita")  # Questo SI

📝 Quiz #7 - Logging

Quale livello di logging useresti per registrare un errore che impedisce il completamento di un'operazione ma non blocca l'intera applicazione?
  • A) WARNING
  • B) ERROR
  • C) CRITICAL
  • D) INFO

🐼8. Introduzione a Pandas (Opzionale)

Pandas è la libreria Python più usata per l'analisi e manipolazione di dati tabulari. È costruita sopra NumPy e fornisce strutture dati ad alte prestazioni (Series e DataFrame) simili a fogli di calcolo o tabelle SQL, ma con la potenza della programmazione Python.

Questa è un'introduzione base a Pandas. Per padroneggiare completamente questa libreria serve un corso dedicato, ma qui vedremo le operazioni fondamentali per leggere, manipolare ed esportare dati.

8.1 Installazione e Importazione

# Installazione
pip install pandas

# Importazione (convenzione standard)
import pandas as pd
import numpy as np  # Spesso usato insieme a Pandas

8.2 DataFrame: Il Cuore di Pandas

import pandas as pd

# ==== CREARE UN DATAFRAME ====

# Da dizionario
dati = {
    'nome': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'età': [25, 30, 35, 28],
    'città': ['Roma', 'Milano', 'Napoli', 'Torino'],
    'stipendio': [30000, 35000, 40000, 32000]
}

df = pd.DataFrame(dati)
print(df)

# Output:
#       nome  età    città  stipendio
# 0    Alice   25     Roma      30000
# 1      Bob   30   Milano      35000
# 2  Charlie   35   Napoli      40000
# 3    Diana   28   Torino      32000

# ==== INFORMAZIONI BASE ====
print(df.shape)         # (4, 4) - righe x colonne
print(df.columns)       # Nomi colonne
print(df.dtypes)        # Tipi di dati
print(df.info())        # Info dettagliate
print(df.describe())    # Statistiche descrittive

8.3 Leggere e Scrivere File

import pandas as pd

# ==== LEGGERE CSV ====
df = pd.read_csv('dati.csv')
df = pd.read_csv('dati.csv', sep=';')  # Delimitatore personalizzato
df = pd.read_csv('dati.csv', encoding='utf-8')

# ==== SCRIVERE CSV ====
df.to_csv('output.csv', index=False)  # index=False per non salvare gli indici

# ==== LEGGERE EXCEL ====
df = pd.read_excel('dati.xlsx', sheet_name='Foglio1')

# ==== SCRIVERE EXCEL ====
df.to_excel('output.xlsx', sheet_name='Dati', index=False)

# ==== LEGGERE JSON ====
df = pd.read_json('dati.json')

# ==== SCRIVERE JSON ====
df.to_json('output.json', orient='records', indent=4)

# ==== LEGGERE SQL ====
import sqlite3
conn = sqlite3.connect('database.db')
df = pd.read_sql_query("SELECT * FROM users", conn)
conn.close()

8.4 Operazioni Base

import pandas as pd

df = pd.DataFrame({
    'nome': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'età': [25, 30, 35, 28],
    'stipendio': [30000, 35000, 40000, 32000]
})

# ==== SELEZIONE ====
print(df['nome'])              # Seleziona colonna
print(df[['nome', 'età']])     # Seleziona più colonne
print(df.loc[0])               # Seleziona riga per label
print(df.iloc[0])              # Seleziona riga per posizione

# ==== FILTRAGGIO ====
over_30 = df[df['età'] > 30]
print(over_30)

# Filtri multipli
filtered = df[(df['età'] > 25) & (df['stipendio'] > 32000)]
print(filtered)

# ==== AGGIUNGERE COLONNE ====
df['bonus'] = df['stipendio'] * 0.1
df['senior'] = df['età'] >= 30

# ==== ORDINAMENTO ====
df_sorted = df.sort_values('età', ascending=False)
print(df_sorted)

# ==== AGGREGAZIONE ====
print(df['stipendio'].mean())    # Media
print(df['età'].max())          # Massimo
print(df['stipendio'].sum())    # Somma

# ==== GROUPBY ====
df_grouped = df.groupby('senior')['stipendio'].mean()
print(df_grouped)

8.5 Esempio Pratico: Analisi Vendite

import pandas as pd

# Carica dati vendite
df = pd.read_csv('vendite.csv')

# Informazioni generali
print("Righe totali:", len(df))
print("\nPrime 5 righe:")
print(df.head())

# Converti la colonna data in datetime
df['data'] = pd.to_datetime(df['data'])

# Calcola ricavo totale
df['ricavo'] = df['quantità'] * df['prezzo']

# Statistiche per prodotto
stats_prodotto = df.groupby('prodotto').agg({
    'quantità': 'sum',
    'ricavo': ['sum', 'mean'],
    'data': 'count'  # Numero transazioni
})
print("\n=== Statistiche per Prodotto ===")
print(stats_prodotto)

# Top 5 prodotti per ricavo
top_5 = df.groupby('prodotto')['ricavo'].sum().sort_values(ascending=False).head(5)
print("\n=== Top 5 Prodotti ===")
print(top_5)

# Vendite per mese
df['mese'] = df['data'].dt.to_period('M')
vendite_mensili = df.groupby('mese')['ricavo'].sum()
print("\n=== Vendite Mensili ===")
print(vendite_mensili)

# Esporta report
stats_prodotto.to_excel('report_prodotti.xlsx')
vendite_mensili.to_csv('vendite_mensili.csv')
💡 Pandas è Potentissimo!
Questa è solo la punta dell'iceberg. Pandas può:
  • Gestire milioni di righe efficientemente
  • Fare join e merge come SQL
  • Gestire dati mancanti (NaN)
  • Fare pivot tables e reshaping
  • Integrare con librerie di visualizzazione (Matplotlib, Seaborn)
  • Fare time series analysis avanzate

🎓Conclusione

🏆 Complimenti!

Hai completato questa lezione approfondita sul File Handling & Data Processing! Ora possiedi competenze fondamentali per:

  • File TXT: Leggere e scrivere file di testo, gestire encoding, usare context manager
  • JSON: Serializzare e deserializzare dati strutturati, gestire API
  • CSV: Lavorare con dati tabulari, usare DictReader/Writer
  • XML: Parsing e creazione di documenti XML gerarchici
  • YAML: Gestire configurazioni leggibili e manutenibili
  • Exception Handling: Creare applicazioni robuste con gestione errori avanzata
  • Logging: Implementare sistemi di logging professionali con livelli e rotazione
  • Pandas (base): Analizzare e manipolare dati tabulari

🚀 Prossimi Passi

Per consolidare queste competenze:

  1. Pratica con Progetti Reali: Crea un sistema di gestione inventario, un'applicazione di log analysis, o un tool di data migration
  2. Esplora Pandas più a Fondo: Se lavori con dati, Pandas è indispensabile. Studia merge, pivot tables, e time series
  3. Impara Formati Avanzati: Parquet (big data), HDF5 (scientific data), Protocol Buffers (performance)
  4. Studia Database: SQL e NoSQL per persistenza dati scalabile
  5. API Development: Costruisci REST API che producono/consumano JSON

📚 Risorse Consigliate

  • Documentazione Python: docs.python.org
  • Pandas Documentation: pandas.pydata.org
  • Real Python: Tutorial approfonditi su tutti i formati
  • JSON Schema: Validare documenti JSON
  • YAML Spec: yaml.org per sintassi completa